fix: resolve ABI generation failure for contracts with unguarded #[no_mangle] functions#401
fix: resolve ABI generation failure for contracts with unguarded #[no_mangle] functions#401
Conversation
…_mangle] functions Contracts with hand-written #[no_mangle] functions that call NEAR host imports (e.g. input, storage_write) fail during ABI generation because the native dylib build cannot resolve these wasm-only symbols. This fixes the problem in the tooling so contract authors don't need to add #[cfg] guards to their code: - On all unix platforms, a small shim library with no-op stubs for NEAR host functions is compiled on the fly and pre-loaded with RTLD_GLOBAL before dlopen'ing the contract dylib, allowing symbol resolution. - On macOS, the linker also rejects undefined symbols in shared libraries, so -undefined dynamic_lookup is passed via RUSTFLAGS during the ABI generation step only. Closes #317
There was a problem hiding this comment.
Pull request overview
This PR fixes ABI generation failures for contracts that include unguarded #[no_mangle] functions referencing NEAR host imports by making the ABI-generation tooling able to load such native dylibs successfully on Unix/macOS.
Changes:
- Add Unix-side preloading of a generated shim dylib that defines NEAR host function symbols so
dlopencan resolve them during ABI extraction. - Add macOS-only linker flags (
-undefined dynamic_lookup) for ABI-generation compilation to allow unresolved host symbols at link time. - Add integration tests covering ABI extraction and full build+deploy+call for a contract containing an unguarded
#[no_mangle]function callingnear_sdk::sys::input.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
cargo-near-build/src/near/abi/generate/mod.rs |
Inject macOS-specific RUSTFLAGS for ABI-generation compilation to allow undefined symbols. |
cargo-near-build/src/near/abi/generate/dylib.rs |
Preload a generated shim dylib on Unix to satisfy missing NEAR host symbols before dlopen. |
integration-tests/tests/abi/e2e.rs |
Add ABI extraction regression test for unguarded #[no_mangle] host-import usage. |
integration-tests/tests/build/opts.rs |
Add end-to-end build+deploy+view-call test for the same scenario. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if let Some(rustflags) = abi_generation_rustflags(hide_warnings) { | ||
| compile_env.push((env_keys::RUSTFLAGS, rustflags)); | ||
| hide_warnings_for_compile = false; |
There was a problem hiding this comment.
On macOS this unconditionally pushes a new RUSTFLAGS value into compile_env, which will override any RUSTFLAGS already present in the env slice (because cargo_native::compile::run collects env into a map). This is inconsistent with the WASM build path (where user-provided env vars are allowed to override tool defaults) and can break projects that rely on custom RUSTFLAGS during ABI generation. Consider reading any existing RUSTFLAGS from env and appending -C link-arg=-undefined -C link-arg=dynamic_lookup (and -Awarnings when needed) rather than replacing the value entirely.
| if let Some(rustflags) = abi_generation_rustflags(hide_warnings) { | |
| compile_env.push((env_keys::RUSTFLAGS, rustflags)); | |
| hide_warnings_for_compile = false; | |
| let has_rustflags_override = env.iter().any(|(key, _)| *key == env_keys::RUSTFLAGS); | |
| if !has_rustflags_override { | |
| if let Some(rustflags) = abi_generation_rustflags(hide_warnings) { | |
| compile_env.push((env_keys::RUSTFLAGS, rustflags)); | |
| hide_warnings_for_compile = false; | |
| } |
| // User-authored #[no_mangle] functions can reference NEAR host imports. | ||
| // Those symbols are unresolved on host and would make dlopen fail before | ||
| // we can call __near_abi_* export functions. Load a small shim library | ||
| // with no-op definitions so ABI extraction can proceed. | ||
| #[cfg(unix)] | ||
| let _host_function_stubs = load_near_host_function_stubs()?; | ||
|
|
There was a problem hiding this comment.
load_near_host_function_stubs() is executed on every ABI extraction on Unix, which means spawning rustc and compiling a cdylib even when the target dylib has no unresolved host symbols. This adds noticeable overhead to cargo near abi/cargo near build (and to integration tests) and may also complicate environments with restricted process spawning. Consider compiling/loading the stubs lazily only when dlopen fails due to an undefined symbol, or caching the compiled+loaded stubs with a OnceLock so it happens at most once per process.
|
I need to benchmark this to make sure there's limited performance degradation |
Summary
Fixes #317. Supersedes #384.
Contracts with hand-written
#[no_mangle]functions that reference NEAR host imports (e.g.input,storage_write) fail during ABI generation because those symbols only exist in the wasm runtime, not on the native host.This fixes the problem in the tooling so contract authors don't need to add
#[cfg]guards to their code:RTLD_GLOBALbeforedlopen'ing the contract dylib, so the dynamic linker can resolve the missing symbols-undefined dynamic_lookupis passed via RUSTFLAGS during the ABI generation step onlycompile.rs— the fix is fully scoped to the ABI generation pathTest plan
test_abi_with_unguarded_no_mangle_function— verifies ABI extraction succeeds for a contract with an unguarded#[no_mangle]fn callingnear_sdk::sys::input()test_build_with_unguarded_no_mangle_function— verifies full build + deploy + call succeeds for the same contract